﻿/***********************************************************************************************
COPYRIGHT 2008 Vijeth D

This file is part of Handwritten Character Recognition - NeuronDotNet Sample.
(Project Website : http://neurondotnet.freehostia.com)

NeuronDotNet is a free software. You can redistribute it and/or modify it under the terms of
the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.

NeuronDotNet is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with NeuronDotNet.
If not, see <http://www.gnu.org/licenses/>.

***********************************************************************************************/

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.Serialization;

namespace NeuronDotNet.Controls
{
    [Serializable]
    public class Stroke : ISerializable
    {
        private Point startingPoint;
        private Point endingPoint;
        private double length;
        private int top;
        private int bottom;
        private int left;
        private int right;
        private bool isHorizontal;
        private bool isVertical;
        private bool isSplitPart;
        private bool isSplit;
        private bool isUpToDate;

        private readonly List<Line> lines = new List<Line>();
        private readonly List<Point> points = new List<Point>();
        private static readonly int tinyThreshold = 5;

        public Point StartingPoint
        {
            get { return startingPoint; }
        }

        public Point EndingPoint
        {
            get { return endingPoint; }
        }

        public int PointsCount
        {
            get { return points.Count; }
        }

        public int LinesCount
        {
            get { return lines.Count; }
        }

        public IEnumerable<Point> Points
        {
            get
            {
                for (int i = 0; i < points.Count; i++)
                {
                    yield return points[i];
                }
            }
        }

        public IEnumerable<Line> Lines
        {
            get
            {
                for (int i = 0; i < lines.Count; i++)
                {
                    yield return lines[i];
                }
            }
        }

        public double Length
        {
            get { return length; }
        }

        public Rectangle Bounds
        {
            get { return new Rectangle(left, top, right - left, bottom - top); }
        }

        public bool IsHorizontal
        {
            get
            {
                Update();
                return isHorizontal;
            }
        }

        public bool IsVertical
        {
            get
            {
                Update();
                return isVertical;
            }
        }

        public bool IsSplitPart
        {
            get { return isSplitPart; }
        }
        
        public bool IsTiny
        {
            get { return (length < tinyThreshold); }
        }

        public Stroke()
        {
            InitializeValues();
        }

        public Stroke(IEnumerable<Point> points, bool isSplitPart)
            : this()
        {
            Helper.ValidateNotNull(points, "points");
            foreach (Point point in points)
            {
                AddPoint(point);
            }
            Update();           
            this.isSplit = true;
            this.isSplitPart = isSplitPart;
        }

        public Stroke(Stroke stroke)
        {
            Helper.ValidateNotNull(stroke, "stroke");
            this.startingPoint = stroke.startingPoint;
            this.endingPoint = stroke.endingPoint;
            this.points.AddRange(stroke.points);

            foreach (Line line in stroke.lines)
            {
                this.lines.Add(new Line(line));
            }

            this.length = stroke.length;

            this.top  = stroke.top;
            this.bottom = stroke.bottom;
            this.left = stroke.left;
            this.right = stroke.right;

            this.isHorizontal = stroke.isHorizontal;
            this.isVertical = stroke.isVertical;
            this.isSplitPart = stroke.isSplitPart;
            this.isSplit = stroke.isSplit;
            this.isUpToDate = stroke.isUpToDate;
        }

        public Stroke(SerializationInfo info, StreamingContext context)
        {
            Helper.ValidateNotNull(info, "info");

            List<Point> pointList = info.GetValue("points", typeof(List<Point>)) as List<Point>;
            for (int i = 0; i < pointList.Count; i++)
            {
                AddPoint(pointList[i]);
            }
            Update();

            this.isSplit = info.GetBoolean("isSplit");
            this.isSplitPart = info.GetBoolean("isSplitPart");
        }

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            Helper.ValidateNotNull(info, "info");
            info.AddValue("points", points, typeof(List<Point>));
            info.AddValue("isSplit", isSplit);
            info.AddValue("isSplitPart", isSplitPart);
        }

        public void InitializeValues()
        {
            startingPoint = Point.Empty;
            endingPoint = Point.Empty;
            length = 0d;

            top = int.MaxValue;
            bottom = 0;
            left = int.MaxValue;
            right = 0;

            isHorizontal = false;
            isVertical = false;
            isSplitPart = false;
            isSplit = false;
            isUpToDate = true;
        }

        public void Clear()
        {
            InitializeValues();
            lines.Clear();
            points.Clear();
        }

        public void AddPoint(Point point)
        {
            // Updates points, lines, startingPoint, endingPoint, length and bounds, isUpToDate and isSplit
            if (points.Count == 0)
            {
                startingPoint = point;
                length = 1d;
            }
            else
            {
                Line line = new Line(endingPoint, point);
                lines.Add(line);
                length += line.Length;
            }
            endingPoint = point;
            points.Add(point);

            left = Math.Min(left, point.X);
            right = Math.Max(right, point.X);
            top = Math.Min(top, point.Y);
            bottom = Math.Max(bottom, point.Y);

            isUpToDate = false;
            isSplit = false;
        }

        public void Reverse()
        {
            Point temp = startingPoint;
            startingPoint = endingPoint;
            endingPoint = temp;

            points.Reverse();
            lines.Reverse();

            foreach (Line line in lines)
            {
                line.Reverse();
            }
        }

        private void Update()
        {
            if (isUpToDate)
            {
                return;
            }

            // Obtain a straight line from starting point to ending point
            Line shortestPossible = new Line(startingPoint, endingPoint);

            int xDifference = endingPoint.X - startingPoint.X;
            int yDifference = endingPoint.Y - startingPoint.Y;

            // Reverse the stroke if necessary
            if ((shortestPossible.DiscreteSlope == DiscreteSlope.Vertical && yDifference < 0) ||
                (shortestPossible.DiscreteSlope == DiscreteSlope.Horizontal && xDifference < 0))
            {
                Reverse();
            }

            isHorizontal = false;
            isVertical = false;

            double straightness = shortestPossible.Length / length;

            double[] distance = { 0,    // Horizontal distance in the stroke
                                  0,    // Oblique distance in the stroke
                                  0 };  // Vertical distance in the stroke

            foreach (Line line in lines)
            {
                distance[(int)(line.DiscreteSlope)] += line.Length;
            }

            if (!IsTiny)
            {
                isHorizontal = straightness > 0.73                    // Pseudo straight
                    && (distance[0] > 0)                              // with non-zero horizontal distance
                    && (distance[2] / distance[0]) < 0.2d;            // and negligible vertical distance

                isVertical = straightness > 0.85                      // Pseudo straight
                    && (distance[2] > 0)                              // with non-zero vertical distance
                    && (distance[1] + distance[0]) / distance[2] < 2; // and insignificant horizontal distance
            }
            isUpToDate = true;
        }

        public IList<Stroke> Split()
        {
            if (isSplit)
            {
                return new List<Stroke>(new Stroke[] { this });
            }

            IList<Stroke> splitStrokes = new List<Stroke>();

            // Collinearity Threshold
            double threshold = 1.25d;

            int count = points.Count - 3;
            int lastSplitAt = 0;

            if (count > 7)
            {             
                double[] second = new double[count];
                double[] third = new double[count];

                for (int i = 0; i < count; i++)
                {
                    second[i] = Distance(points[i], points[i + 2]);
                    third[i] = Distance(points[i], points[i + 3]);
                }
               
                for (int i = 3; i < count; i++)
                {
                    bool collinearFirst =
                        (lines[i - 1].Length + lines[i].Length) < threshold * second[i - 1];

                    bool collinearSecond =
                        (second[i - 2] + second[i]) < threshold * Distance(points[i - 2], points[i + 2]);

                    bool collinearThird =
                        (third[i - 3] + third[i]) < threshold * Distance(points[i - 3], points[i + 3]);

                    if (!(
                        (collinearFirst || collinearSecond) &&
                        (collinearSecond || collinearThird) &&
                        (collinearSecond || collinearFirst)))
                    {
                        splitStrokes.Add(new Stroke(points.GetRange(lastSplitAt, i - lastSplitAt), true));
                        lastSplitAt = i;
                    }
                }
            }
            if (lastSplitAt == 0)
            {
                splitStrokes.Add(this);
            }
            else
            {
                splitStrokes.Add(new Stroke(points.GetRange(lastSplitAt, points.Count - lastSplitAt), true));
            }
            isSplit = true;
            return splitStrokes;
        }

        private static double Distance(Point source, Point destination)
        {
            int xDifference = destination.X - source.X;
            int yDifference = destination.Y - source.Y;

            return Math.Sqrt(xDifference * xDifference + yDifference * yDifference);
        }

        public void Clip(Rectangle bounds)
        {
            List<Point> pointSet = new List<Point>();
            Line clipped = null;
            for (int i = 0; i < lines.Count; i++)
            {
                clipped = Line.Clip(bounds, lines[i]);
                if (clipped != null)
                {
                    pointSet.Add(clipped.Start);
                }
            }
            if (clipped != null)
            {
                pointSet.Add(clipped.End);
            }
            Clear();
            for (int i = 0; i < pointSet.Count; i++)
            {
                AddPoint(pointSet[i]);
            }
        }
    }
}
